﻿using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;
using Nemerle.Data;

using System;
using System.Transactions;
using System.Data;
using System.Collections.Generic;
using System.Linq;
using NRails;
using NRails.Database;
using NRails.Database.Schema;
using BLToolkit.Data;
using BLToolkit.Data.Linq;
using BLToolkit.Data.Sql;
using BLToolkit.DataAccess;
using Nemerle.Logging;

namespace NRails.Migrations
{
  public variant MigrationDirection
  {
      | All
      | Up
      | Down
      | Version { version : string }
  }

  public class Migrator
  {
      public static SchemaMigrations = "SchemaMigrations" : string;
      engine : Engine;
      
      class InitialMigration : Migration
      {
          public this () 
          {
              base("init!");
          }
          
          protected override Up() : void
          {
              this.CreateTable(SchemaMigrations, t => 
              {
                  t.Add(c => 
                  {
                      c.Name = "Version";
                      c.Type = SqlDataType.String;
                      c.Nullable = false;
                  });
                  t.AddPk("Version");
              });
          }
      }
      
      public this(engine : Engine)
      {
          this.engine = engine;
      }
      
      public GetSchema() : DBSchema
      {
          log(Debug, "begin create migration");
          def dbDriver = engine.CreateDbDriver();
          def connString = engine.Cfg.ConnectionString.ConnectionString;
          def schemaDriver = dbDriver.CreateSchemaDriver();
          
          if (schemaDriver.DatabaseExists(connString))
          {
              def schema = schemaDriver.LoadExistingSchema(connString);
              log(Debug, "schema loaded, end create migration");
              schema
          }
          else
          {
              log(Info, "Database dont exists");
              null
          }
      }
      
      public Migrate(migrationDirection : MigrationDirection, migrs : list[IMigration], allowMissing : bool) : void
      {
          log(Debug, "begin migrations");
          def dbDriver = engine.CreateDbDriver();
          def connString = engine.Cfg.ConnectionString.ConnectionString;
          def schemaDriver = dbDriver.CreateSchemaDriver();
          
          unless (schemaDriver.DatabaseExists(connString))
          {
              schemaDriver.CreateDatabase(connString);
              log(Info, "database created");
          }
          
          def schema = schemaDriver.LoadExistingSchema(connString);
          log(Debug, "schema loaded");

          def sort(ms : list[IMigration]) : list[IMigration]
          {
              ms.Sort((m, m2) => m.Version.CompareTo(m2.Version))
          }

          def cont = match (migrationDirection)
          {
            | MigrationDirection.Version(version) => migrs.Any(m => m.Version == version) || allowMissing
            | _ => true
          }
          
          if (cont)
          {
              mutable exactMigrationDirection = MigrationDirection.Up();
              def migrations = if (!schema.Tables.Any(t => t.Name == SchemaMigrations))
              {
                  InitialMigration() :: match (migrationDirection)
                  {
                      | MigrationDirection.All => sort(migrs)
                      | MigrationDirection.Up => sort(migrs).First() :: []
                      | MigrationDirection.Version(version) => sort(migrs.Filter(m => m.Version.CompareTo(version) <= 0))
                      | MigrationDirection.Down => log(Info, "Can't go down - no migrations in database"); []
                  }
              }
              else
              {
                  def existing = using (conn = schemaDriver.CreateConnection(connString))
                  {
                      conn.Open();
                      using (db = DbManager(conn))
                      {
                          db.GetTable.[SchemaMigrations]().Select(sm => sm.Version).ToArray()
                      }
                  }

                  def toMigr(version : string) : IMigration
                  {
                      migrs.First(m => m.Version == version)
                  }
                  
                  def nonExisting() : list[IMigration]
                  {
                      migrs.Filter(m => !existing.Contains(m.Version))
                  }
                  
                  match (migrationDirection)
                  {
                      | MigrationDirection.All => sort(nonExisting())
                      | MigrationDirection.Up =>
                        def sortedFiltered = sort(nonExisting());
                        match (sortedFiltered.Any())
                        {
                            | true => sortedFiltered.First() :: []
                            | false => log(Info, "Can't go up - all migrations done"); []
                        }
                      | MigrationDirection.Down =>
                        exactMigrationDirection = MigrationDirection.Down();
                        match (existing.Any())
                        {
                            | true => toMigr(existing.Max()) :: []
                            | false => log(Info, "Can't go down - no migrations in database"); []
                        }
                      | MigrationDirection.Version(version) => 
                        match (existing.Contains(version))
                        {
                            | true => exactMigrationDirection = MigrationDirection.Down();
                                      sort(existing.Filter(v => v.CompareTo(version) > 0).Map(v => toMigr(v))).Rev()
                            | false => sort(nonExisting().Filter(m => m.Version.CompareTo(version) <= 0))
                        }
                  }
              }

              match (exactMigrationDirection)
              {
                  | MigrationDirection.Up => log(Info, "going up")
                  | MigrationDirection.Down => log(Info, "going down")
                  | _ => throw InvalidOperationException($"Expected Up/Down, not $(exactMigrationDirection)");
              }

              log(Info, $"$(migrations.Length) migration(s) to do");
              when (migrations.Length > 0)
                RunMigrations(exactMigrationDirection, migrations, schema, schemaDriver, connString);

              log(Debug, "end migrations");
          }
          else
          {
              log(Info, "Missing migrations is not allowed")
          }
      }
      
      RunMigrations(exactMigrationDirection : MigrationDirection, migrations : list[IMigration], schema : DBSchema, schemaDriver : IDBSchemaDriver, connString : string) : void
      {
          foreach (m in migrations)
          {
              log(Info, $"Start migration '$(m.Version)'");
              try 
              {
                  def tx = if (m.NeedScope) TransactionScope() else null;
                  try
                  {
                      using (conn = schemaDriver.CreateConnection(connString), db = DbManager(conn))
                      {
                          conn.Open();
                          using (m.HandlerScope(ExecuteAction(conn, schema, schemaDriver, _)))
                          {
                              m.AssignSchema(schema, db);
              
                              match (exactMigrationDirection)
                              {
                                  | MigrationDirection.Up => m.Up()
                                  | MigrationDirection.Down => m.Down()
                                  | _ => throw NotSupportedException();
                              }
                          }
                          
                          when (!m.Version.EndsWith("!"))
                              {
                                  def _dir = match (exactMigrationDirection)
                                  {
                                      | MigrationDirection.Up => "insert"
                                      | MigrationDirection.Down => "delete"
                                      | _ => throw NotSupportedException();
                                  }
                                  log(Debug, $"$(_dir) migration info $(m.Version)");
                                  def version = SchemaMigrations();
                                  version.Version = m.Version;
                                  _ = match (exactMigrationDirection)
                                  {
                                      | MigrationDirection.Up => db.Insert(version)
                                      | MigrationDirection.Down => db.Delete(version)
                                      | _ => throw NotSupportedException();
                                  }
                                  log(Debug, $"migration info $(_dir) finished");
                              }
                          when (m.NeedScope)
                          tx.Complete();
                      }
                      log(Debug, $"Finish migration '$(m.Version)'");
                  }
                  finally
                  {
                      when(m.NeedScope)
                          tx.Dispose();
                  }
              }
              catch 
              {
                  | _ => throw; // todo: log, show error
              }
          }
      }
      
      ExecuteDdl(conn : IDbConnection, ddl : string) : void
      {
          using (cmd = conn.CreateCommand())
          {
              //cmd.Transaction = trans;
              cmd.CommandText = ddl;
              log(Info, $"$ddl");
              _ = cmd.ExecuteNonQuery();
          }
      }
      
      ExecuteAction(conn : IDbConnection, schema : DBSchema, schemaDriver : IDBSchemaDriver, ddlAction : MigrationAction) : void
      {
          def ddlHandler(ddl)
          {
              ExecuteDdl(conn, ddl);
          }
          
          def validateCreate(columnAction) {
               | TableAction.AddColumn => ()
               | TableAction.CreateIndex => ()
               | _ => throw InvalidOperationException("Only new columns or indicies allowed in create table.");
          }

          def processChange(table, columnAction) {
               match (columnAction)
               {
                   | TableAction.AddColumn(col) => 
                        ddlHandler(schemaDriver.MakeDdlColumnCreate(col, table));
                        
                   | TableAction.ChangeColumn(col, newCol) =>
                        ddlHandler(schemaDriver.MakeDdlColumnAlter(col, newCol, table));
                        
                   | TableAction.RenameColumn(col, _, newName) =>
                        ddlHandler(schemaDriver.MakeDdlColumnRename(col, newName, table));
                        
                   | TableAction.DropColumn(col) =>
                        when (table.Keys != null)
                            foreach (key in table.Keys.Where(k => k.Columns == col.Name))
		                    {
		                        ddlHandler(schemaDriver.MakeDdlKeyDrop(key, table))
		                    }

                        ddlHandler(schemaDriver.MakeDdlColumnDrop(col, table));

                   | TableAction.CreateIndex(idx) =>
                        ddlHandler(schemaDriver.MakeDdlIndexCreate(idx, table));
                        
                   | TableAction.DropIndex(idx) =>
                        ddlHandler(schemaDriver.MakeDdlIndexDrop(idx, table));
                        
                   | _ => throw InvalidOperationException(columnAction.ToString());
               }
          }
          
          match (ddlAction)
          {
            |  MigrationAction.CreateTable(table, actions) => 
                actions.Iter(validateCreate);
                ddlHandler(schemaDriver.MakeDdlTableCreate(table, true));
                table.Indexes.Iter(idx => ddlHandler(schemaDriver.MakeDdlIndexCreate(idx, table)));
                
            |  MigrationAction.RenameTable(table, newName) => 
                ddlHandler(schemaDriver.MakeDdlTableRename(table, newName));
                table.Name = newName;
                
            |  MigrationAction.ChangeTable(table, actions) => 
                _ = schemaDriver.ReloadTableSchema(conn, schema, table);
                actions.Iter(processChange(table, _));
                
            |  MigrationAction.DropTable(table) => 
                ddlHandler(schemaDriver.MakeDdlTableDrop(table));
                _= schema.Tables.Remove(table);
          }
      }
  }
}
